#include "../../syncup/fsnotify_imp.h"
#include <common.h>
#include <utils.h>

#include <poll.h>
#include <dirent.h>


int
fsnotify_desc_open(struct fsnotify_desc *desc)
{
    desc->fd = inotify_init();
    if (desc->fd < 0) {
        g_critical("can't initialize inotify instance: %s", strerror(errno));
        return -1;
    }
    desc->wd_map = g_hash_table_new(g_int_hash, g_int_equal);
    desc->buf = g_string_sized_new(4096);
    return 0;
}

void
fsnotify_desc_close(struct fsnotify_desc *desc)
{
    close(desc->fd);
    g_hash_table_destroy(desc->wd_map);
    g_string_free(desc->buf, TRUE);
}


#define FSNOTIFY_WATCH_EVENT \
    (IN_CREATE | IN_DELETE | IN_MODIFY | IN_MOVED_FROM | IN_MOVED_TO)

int
fsnotify_watch_dentry(struct fsnotify_desc *desc, struct dentry *d,
        GString *path)
{
    g_assert(d != NULL);
    g_assert(path != NULL);

    //g_debug("inotify_add_watch path: %s", path->str);
    int wd = inotify_add_watch(desc->fd, path->str, FSNOTIFY_WATCH_EVENT);
    if (wd < 0) {
        g_warning("can't add inotify watch: %s", strerror(errno));
        return -1;
    }
    d->wd = wd;
    g_hash_table_insert(desc->wd_map, &d->wd, d);
    return 0;
}

int
fsnotify_watch_dentry_recursively(struct fsnotify_desc *desc, struct dentry *d,
        GString *path)
{
    DIR *dir;
    struct dirent *dp;
    gsize end = path->len;

    if (fsnotify_watch_dentry(desc, d, path) < 0) {
        return -1;
    }

    dir = opendir(path->str);
    if (dir == NULL) {
        g_critical("can't open directory '%s': %s", path->str,
                strerror(errno));
        return -1;
    }
    while ((dp = readdir(dir)) != NULL) {
        if (strcmp(dp->d_name, ".") == 0 || strcmp(dp->d_name, "..") == 0) {
            continue;
        }
        if (dp->d_type == DT_DIR) {
            struct dentry *d_child = dentry_new(dp->d_name);
            dentry_add_child(d, d_child);
            path_append_component(path, dp->d_name);
            fsnotify_watch_dentry_recursively(desc, d_child, path);
            g_string_truncate(path, end);
        }
    }
    closedir(dir);
    return 0;
}

void
fsnotify_rm_watch_dentry(struct fsnotify_desc *desc, struct dentry *d)
{
    inotify_rm_watch(desc->fd, d->wd);
    g_hash_table_remove(desc->wd_map, &d->wd);
}

static void
fsnotify_rm_watch_dentry_recursively0(const void *unused_key,
        struct dentry *d, struct fsnotify_desc *desc)
{
    fsnotify_rm_watch_dentry(desc, d);
    if (d->child) {
        g_tree_foreach(d->child,
                (GTraverseFunc) fsnotify_rm_watch_dentry_recursively0,
                desc);
    }
}

void
fsnotify_rm_watch_dentry_recursively(struct fsnotify_desc *desc,
        struct dentry *d)
{
    fsnotify_rm_watch_dentry_recursively0(NULL, d, desc);
}

#define EVENT_SIZE (sizeof (struct inotify_event))
#define EVENT_GUESS_SIZE (EVENT_SIZE + 16)
#define EVENT_BUF_SIZE (1024 * EVENT_GUESS_SIZE)
#define EVENT_REAL_SIZE(evt) (EVENT_SIZE + (evt)->len)

#define NEXT_EVENT_POS(pos, evt) ((pos) + EVENT_REAL_SIZE(evt))

static char evt_buf[EVENT_BUF_SIZE];

/**
 * @internal
 * @brief Read next \c inotify_event
 *
 * \p pos and \p len may change if \c evt_buf is re-read.
 *
 * @param pos  Position, set to the index of next event in advance
 *
 * @note This function is only called on receiving \c IN_MOVED_FROM to check
 * if there is corresponding \c IN_MOVED_FROM. Here is an assumption that
 * related \c IN_MOVED_FROM and \c IN_MOVED_TO appear in adjacency.
 */
static inline struct inotify_event *
read_next_event(struct fsnotify_desc *desc, ptrdiff_t *pos, ssize_t *len,
        struct inotify_event *evt)
{
    if (*pos >= *len) {
        ssize_t ret;
        struct pollfd fds[1];
        fds[0].fd = desc->fd;
        fds[0].events = POLLIN;
//        g_debug("read_next_event(): before poll");
        /* In practice, corresponding IN_MOVED_TO may not be available
         * immediately, so waitting for a reasonable time period is necessary.
         */
        if (poll(fds, 1, 10) <= 0) {
            return NULL;
        }
//        g_debug("read_next_event(): poll success");

        /* Read from start; assert evt is unused so can be overwritten */
        *pos = 0;
        *len = 0;
        ret = read(desc->fd, evt_buf, EVENT_BUF_SIZE);
        if (ret < 0) {
            g_critical("can't read inotify_event: %s", strerror(errno));
            return NULL;
        }
        *len = ret;
    }
    return (struct inotify_event *) &evt_buf[*pos];
}

#define WD_TO_DENTRY(wd, desc) \
    ((struct dentry *) g_hash_table_lookup((desc)->wd_map, &(wd)))

/**
 * @brief Convert \c inotify_event to \c fsnotify_event
 *
 * \p pos is set to the index of next \c inotify_event
 */
static inline struct fsnotify_event *
convert_inotify_evt_to_fsnotify_evt(struct fsnotify_desc *desc,
        ptrdiff_t *pos, ssize_t *len, struct inotify_event *evt)
{
    struct fsnotify_event *fs_evt;
    struct dentry *d;

    fs_evt = fsnotify_event_new(evt->mask);
    if (evt->len > 0) {
        fsnotify_event_set_name(fs_evt, evt->name);
    }

    *pos = NEXT_EVENT_POS(*pos, evt);

    /* NOTE: It's important to check any returned value of WD_TO_DENTRY,
     * because there are some race conditions that causes it to be NULL.
     * For example, a watched directory is moved out. Before we read events
     * into queue, some file is created in sub directory. Therefore, when we
     * read events, we get moved out event and file create event that points
     * to a dirent which will be removed due to moved out event.
     */
    if (evt->mask & (IN_CREATE | IN_DELETE | IN_MODIFY)) {
        d = WD_TO_DENTRY(evt->wd, desc);
        if (G_UNLIKELY(d == NULL)) {
            goto invalid_event_exit;
        }
        fs_evt->d.dir = d;
    } else if (evt->mask & IN_MOVED_FROM) {
        /* Assumption: IN_MOVED_FROM's corresponding IN_MOVED_TO must be next
         * to it. If not found, it means moved out event.
         */
        struct inotify_event *next_evt;
        uint32_t cookie = evt->cookie;

        d = WD_TO_DENTRY(evt->wd, desc);
        if (G_UNLIKELY(d == NULL)) {
            goto invalid_event_exit;
        }
        fs_evt->d.rename.from = d;
        next_evt = read_next_event(desc, pos, len, evt);
        if (next_evt == NULL) {
            fs_evt->d.rename.to = NULL;
        } else {
            if (next_evt->cookie == cookie
                    && (next_evt->mask & IN_MOVED_TO)) {
                /* Consume the next event */
                *pos = NEXT_EVENT_POS(*pos, next_evt);

                fs_evt->mask |= IN_MOVED_TO;
                d = WD_TO_DENTRY(next_evt->wd, desc);
                if (G_UNLIKELY(d == NULL)) {
                    goto invalid_event_exit;
                }
                if (strcmp(fs_evt->name, next_evt->name) != 0) {
                    fsnotify_event_set_new_name(fs_evt, next_evt->name);
                }
                fs_evt->d.rename.to = d;
            } else { /* irrelevant event */
                fs_evt->d.rename.to = NULL;
            }
        }
    } else if (evt->mask & IN_MOVED_TO) {
        /* According to the above assumption, this is moved in event. */
        fs_evt->d.rename.from = NULL;
        d = WD_TO_DENTRY(evt->wd, desc);
        if (G_UNLIKELY(d == NULL)) {
            goto invalid_event_exit;
        }
        fs_evt->d.rename.to = d;
    } else {
        g_critical("unknown inotify_event (mask: 0x%x)", evt->mask);
        goto invalid_event_exit;
    }

    return fs_evt;

invalid_event_exit:
    fsnotify_event_free(fs_evt);
    return NULL;
}

int
fsnotify_read_event(struct fsnotify_desc *desc, struct list_head *evt_queue)
{
    ptrdiff_t pos;
    ssize_t len = read(desc->fd, evt_buf, EVENT_BUF_SIZE);
    if (len < 0) {
        if (errno != EINTR) {
            g_critical("can't read inotify_event: %s", strerror(errno));
        }
        return -1;
    }

    pos = 0;
    while (pos < len) {
        struct fsnotify_event *fs_evt;
        {
            struct inotify_event *evt;
            evt= (struct inotify_event *) &evt_buf[pos];

            if (evt->mask & IN_IGNORED) {
                pos = NEXT_EVENT_POS(pos, evt);
                continue;
            }

            fs_evt = convert_inotify_evt_to_fsnotify_evt(desc, &pos, &len,
                    evt);
        }
        if (fs_evt == NULL) {
            continue;
        }

        /*
         * Process dentry tree if needed (FS_ISDIR is set)
         */
        if (fs_evt->mask & FS_ISDIR) {
            struct dentry *d_child;

            if (fs_evt->mask & FS_CREATE) {
                d_child = dentry_new(fs_evt->name);
                dentry_add_child(fs_evt->d.dir, d_child);
                dentry_get_path(d_child, desc->buf);
                fsnotify_watch_dentry(desc, d_child, desc->buf);
            } else if (fs_evt->mask & FS_DELETE) {
                d_child = dentry_lookup_child(fs_evt->d.dir, fs_evt->name);
                /* Due to unfixable bug of event miss, d_child can be NULL */
                if (G_LIKELY(d_child)) {
                    fsnotify_rm_watch_dentry(desc, d_child);
                    fs_evt->garbage = dentry_steal_child(fs_evt->d.dir,
                            fs_evt->name);
                }
            } else if ((fs_evt->mask & FS_MOVE) == FS_MOVE) {
                d_child = dentry_lookup_child(fs_evt->d.rename.from,
                        fs_evt->name);
                if (G_LIKELY(d_child)) {
                    if (fs_evt->new_name) {
                        dentry_rename(d_child, fs_evt->new_name);
                    }
                    dentry_move_child(fs_evt->d.rename.from,
                            fs_evt->d.rename.to, d_child);
                }
            } else if ((fs_evt->mask & FS_MOVE) == FS_MOVED_TO) {
                d_child = dentry_new(fs_evt->name);
                dentry_add_child(fs_evt->d.rename.to, d_child);
                dentry_get_path(d_child, desc->buf);
                fsnotify_watch_dentry_recursively(desc, d_child, desc->buf);
            } else if ((fs_evt->mask & FS_MOVE) == FS_MOVED_FROM) {
                d_child = dentry_lookup_child(fs_evt->d.rename.from,
                        fs_evt->name);
                /* Due to unfixable bug of event miss, d_child can be NULL */
                if (G_LIKELY(d_child)) {
                    fsnotify_rm_watch_dentry_recursively(desc, d_child);
                    fs_evt->garbage = dentry_steal_child(fs_evt->d.rename.from,
                            fs_evt->name);
                }
            }
        }

        list_add_tail(&fs_evt->evt_list, evt_queue);
    }
    return 0;
}


/* For testing */
struct dentry *
fsnotify_wach_dir_recursively(struct fsnotify_desc *desc, const char *path)
{
    GString *path_buf = g_string_sized_new(4096);
    struct dentry *d;

    g_string_append(path_buf, path);
    d = dentry_new(path);
    fsnotify_watch_dentry_recursively(desc, d, path_buf);

    g_string_free(path_buf, TRUE);
    return d;
}
